﻿using Drive.LiveChat.Model;
using Drive.LiveChat.Server.Controllers;
using Drive.LiveChat.Server.Models;
using Drive.LiveExcelHost.Controllers;
using Drive.LiveExcelHost.Models;
using Drive.LiveExcelHost.Services;
using Drive.Support;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace Drive.LiveExcelHost
{
    class SocketIoBootstrapper
    {
        const int BUFFER_SIZE = 4 * 1024 * 1024; // 4MB
        static ConcurrentDictionary<string, WebSocketSession> sessions = new ConcurrentDictionary<string, WebSocketSession>(); //sid,websocket
        static ConcurrentDictionary<long, List<MessageForThrottle>> messages = new ConcurrentDictionary<long, List<MessageForThrottle>>();//uid,sendMessages

        internal static List<WebSocketSession> GetWebSocketSessions(int visitorId, bool isAgentSession)
        {
            return sessions.Values
                .Where(x => x.VisitorId == visitorId)
                .Where(x => x.HandshakeCompleted)
                .Where(x => x.isAgentSession == isAgentSession)
                .ToList();
        }
        internal static VisitorInfo DecodeVisitorInfo(string uid)
        {
            if (!string.IsNullOrEmpty(uid))
            {
                try
                {
                    return JsonConvert.DeserializeObject<VisitorInfo>(Crypto.AESDecryptText(uid, coreKey));
                }
                catch (Exception ex)
                {

                }
            }
            return null;
        }
        public static string EncodeVisitorInfo(VisitorInfo visitor)
        {
            var text = JsonConvert.SerializeObject(visitor);
            return Crypto.AESEncryptText(text, coreKey);
        }

        internal static void TryUpgrade(VisitorInfo visitor, ref string sid, Request request, Response response, CancellationToken stoppingToken)
        {
            if (!response.Headers.ContainsKey("Connection"))
            {
                response.Headers["Content-Type"] = "application/octet-stream";
                response.Headers["Access-Control-Allow-Credentials"] = "true";
                if (!string.IsNullOrEmpty(request.Origin))
                    response.Headers["Access-Control-Allow-Origin"] = request.Origin;
                response.Headers["Connection"] = "keep-alive";
            }
            if (string.IsNullOrEmpty(sid))
            {
                ReplaySocketIoUpgradesSupported(response, ref sid, request.Origin);
                messages.TryAdd(visitor.VisitorId, new List<MessageForThrottle>());
                //客户端的第一个ping，告诉客户端支持websocket
            }
            else
            {
                messages.TryAdd(visitor.VisitorId, new List<MessageForThrottle>());
                if (!sessions.TryGetValue(sid, out var session))
                {
                    sessions.TryAdd(sid, new WebSocketSession(null, visitor.VisitorId, sid, messages[visitor.VisitorId], stoppingToken));
                    session = sessions[sid];
                }
                session.isAgentSession = request.Path == "/workbench/";
                if (!session.HandshakeStarted)
                {
                    ReplaySocketIoHandshakeStarted(response);
                    session.HandshakeStarted = true;
                }
                else
                {
                    int waitmilliseconds = 0;
                    int waitinterval = 10;
                    while (!session.HandshakeCompleted)
                    {
                        SpinWait.SpinUntil(() => session.IsReady || stoppingToken.IsCancellationRequested, waitinterval);
                        if (stoppingToken.IsCancellationRequested)
                        {
                            return;
                        }
                        waitmilliseconds += waitinterval;
                        if (waitmilliseconds > 1000)
                            break;
                    }
                    //如果websocket未就绪，允许发起下一次轮询
                    //如果已就绪，告诉客户端使用websocket

                    //jexcel 现发40 后发6
                    if (!session.IsReady)
                    {
                        ReplaySocketIoHandshakeStarted(response);
                    }
                    else
                    {
                        ReplaySocketIoWebSocketSessionUpgraded(response); //jexcel required
                    }
                }
            }
        }

        /// <summary>
        /// 6 - 告诉客户端基于socketio协议的ws会话协议就绪
        /// </summary>
        static void ReplaySocketIoWebSocketSessionUpgraded(Response response)
        {
            //0x01 创建链路？
            response.Write(new byte[] { 0x00, 0x01, 0xff, 0x36 /*6*/ }); //BINARY_ACK?
        }

        /// <summary>
        /// 40 - 告诉客户端基于ws的socketio握手成功。
        /// 必须发送，否则客户端socketio实例状态的connect标志处于false，导致无法发送消息
        /// </summary>
        static void ReplaySocketIoHandshakeStarted(Response response)
        {
            //0x02 心跳？
            response.Write(new byte[] { 0x00, 0x02, 0xff, 0x34, 0x30 /* char-40*/ });
        }

        static string coreKey = "Pk&yo7!#";

        /// <summary>
        /// 告诉客户端支持ws协议
        /// </summary>
        static void ReplaySocketIoUpgradesSupported(Response response, ref string sid, string origin)
        {
            sid = Guid.NewGuid().ToString().Substring(0, 8);
            var content = new List<byte> { 0x00, 0x09, 0x07, 0x0ff };
            var body = "0{\"sid\":\"" + sid + "\",\"upgrades\":[\"websocket\"],\"pingInterval\":25000,\"pingTimeout\":60000} ";
            content.AddRange(Encoding.UTF8.GetBytes(body));
            response.Write(content.ToArray());
        }

        internal static async Task ProcessWebSocketRequest(VisitorInfo visitor, Request request, WebSocket webSocket, CancellationToken stoppingToken)
        {
            //Thread.Sleep(5000);
            var sid = new Regex("sid=(.+?)$").Match(request.PathAndQuery).Groups[1].Value;
            if (sessions.TryGetValue(sid, out var session))
            {
                session.WebSocket = webSocket;
            }
            else
            {
                if (sessions.TryAdd(sid, new WebSocketSession(webSocket, visitor.VisitorId, sid, messages[visitor.VisitorId], stoppingToken)))
                {
                    session = sessions[sid];
                }
                else
                {
                    Debugger.Break();
                }
            }

            SocketIOService service = null;
            if (request.Path.StartsWith("/s/socket.io"))
            {
                service = new ExcelService(request, session);
            }
            else if (session.isAgentSession)
            {
                service = new AgentMessenger(request, session);
            }
            else
            {
                service = new VisitorMessenger(request, session);
            }
            try
            {
                await RespondToWebSocketRequestAsync(service, session, stoppingToken);
            }
            catch (Exception ex)
            {
                var innerError = ex.InnerException as SocketException;
                if (innerError != null)
                {
                    return;
                }
                var innerError2 = ex.InnerException as ObjectDisposedException;
                if (innerError2 != null)
                {
                    return;
                }
                //service.ReportError(ex);
            }
            finally
            {
                sessions.TryRemove(sid, out var session1);
                await service.OnSocketIoSessionDestory();
            }
        }




        public static async Task RespondToWebSocketRequestAsync(SocketIOService service, WebSocketSession session, CancellationToken token)
        {
            ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[BUFFER_SIZE]);
            var httpContext = Drive.LiveExcelHost.Mvc.HttpContext.Current;
            while (true)
            {
                WebSocketReceiveResult result = await session.WebSocket.ReceiveAsync(buffer, token);

                //消息接收线程不确定的变更导致ThreadLocal失效，因此重新赋值
                Drive.LiveExcelHost.Mvc.HttpContext.Current = httpContext;
                if (result.MessageType == WebSocketMessageType.Close)
                {
                    //_logger.LogInformation($"Client initiated close. Status: {result.CloseStatus} Description: {result.CloseStatusDescription}");
                    await service.OnClose(result.CloseStatus);
                    break;
                }

                if (result.Count > BUFFER_SIZE)
                {
                    await session.WebSocket.CloseAsync(WebSocketCloseStatus.MessageTooBig,
                        $"Web socket frame cannot exceed buffer size of {BUFFER_SIZE:#,##0} bytes. Send multiple frames instead.",
                        token);
                    await service.OnClose(WebSocketCloseStatus.MessageTooBig);
                    break;
                }

                // disconnected	0	关闭链路
                // connected	1	创建链路
                // heartbeat	2	心跳
                // pong	3	心跳回应
                // message	4	发送消息
                // upgrade	5	升级完成
                // noop	6	知道了

                // just echo the message back to the client
                ArraySegment<byte> toSend = new ArraySegment<byte>(buffer.Array, buffer.Offset, result.Count);
                var d = Encoding.UTF8.GetString(toSend);
                if (d == "2probe")
                {
                    toSend = TextUtil.TextToBytes("3probe");
                    await session.WebSocket.SendAsync(toSend, WebSocketMessageType.Text, true, token);
                    session.HandshakeCompleted = true;
                }
                else if (d == "5")
                {
                    await service.OnSocketIoSessionCreate();
                    session.IsReady = true;
                    continue;
                }
                else if (d == "2")
                {
                    await service.OnSocketIoPingMessageReceived();

                }
                else if (d.StartsWith("4"))
                {
                    var packet = d.Substring(2);
                    if (!string.IsNullOrEmpty(packet))
                    {
                        await service.OnPacketReceived(packet);
                    }
                    continue;
                }
                else
                {

                }
            }
        }
    }
}
